// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/files/file_path_watcher_fsevents.h"

#include <list>

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/mac/libdispatch_task_runner.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/threading/thread_task_runner_handle.h"

namespace base {

namespace {

    // The latency parameter passed to FSEventsStreamCreate().
    const CFAbsoluteTime kEventLatencySeconds = 0.3;

    class FSEventsTaskRunner : public mac::LibDispatchTaskRunner {
    public:
        FSEventsTaskRunner()
            : mac::LibDispatchTaskRunner("org.chromium.FilePathWatcherFSEvents")
        {
        }

    protected:
        ~FSEventsTaskRunner() override { }
    };

    static LazyInstance<FSEventsTaskRunner>::Leaky g_task_runner = LAZY_INSTANCE_INITIALIZER;

    // Resolve any symlinks in the path.
    FilePath ResolvePath(const FilePath& path)
    {
        const unsigned kMaxLinksToResolve = 255;

        std::vector<FilePath::StringType> component_vector;
        path.GetComponents(&component_vector);
        std::list<FilePath::StringType>
            components(component_vector.begin(), component_vector.end());

        FilePath result;
        unsigned resolve_count = 0;
        while (resolve_count < kMaxLinksToResolve && !components.empty()) {
            FilePath component(*components.begin());
            components.pop_front();

            FilePath current;
            if (component.IsAbsolute()) {
                current = component;
            } else {
                current = result.Append(component);
            }

            FilePath target;
            if (ReadSymbolicLink(current, &target)) {
                if (target.IsAbsolute())
                    result.clear();
                std::vector<FilePath::StringType> target_components;
                target.GetComponents(&target_components);
                components.insert(components.begin(), target_components.begin(),
                    target_components.end());
                resolve_count++;
            } else {
                result = current;
            }
        }

        if (resolve_count >= kMaxLinksToResolve)
            result.clear();
        return result;
    }

} // namespace

FilePathWatcherFSEvents::FilePathWatcherFSEvents()
    : fsevent_stream_(NULL)
{
}

bool FilePathWatcherFSEvents::Watch(const FilePath& path,
    bool recursive,
    const FilePathWatcher::Callback& callback)
{
    DCHECK(MessageLoopForIO::current());
    DCHECK(!callback.is_null());
    DCHECK(callback_.is_null());

    // This class could support non-recursive watches, but that is currently
    // left to FilePathWatcherKQueue.
    if (!recursive)
        return false;

    set_task_runner(ThreadTaskRunnerHandle::Get());
    callback_ = callback;

    FSEventStreamEventId start_event = FSEventsGetCurrentEventId();
    g_task_runner.Get().PostTask(
        FROM_HERE, Bind(&FilePathWatcherFSEvents::StartEventStream, this, start_event, path));
    return true;
}

void FilePathWatcherFSEvents::Cancel()
{
    set_cancelled();
    callback_.Reset();

    // Switch to the dispatch queue thread to tear down the event stream.
    g_task_runner.Get().PostTask(
        FROM_HERE,
        Bind(&FilePathWatcherFSEvents::CancelOnMessageLoopThread, this));
}

// static
void FilePathWatcherFSEvents::FSEventsCallback(
    ConstFSEventStreamRef stream,
    void* event_watcher,
    size_t num_events,
    void* event_paths,
    const FSEventStreamEventFlags flags[],
    const FSEventStreamEventId event_ids[])
{
    FilePathWatcherFSEvents* watcher = reinterpret_cast<FilePathWatcherFSEvents*>(event_watcher);
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());

    bool root_changed = watcher->ResolveTargetPath();
    std::vector<FilePath> paths;
    FSEventStreamEventId root_change_at = FSEventStreamGetLatestEventId(stream);
    for (size_t i = 0; i < num_events; i++) {
        if (flags[i] & kFSEventStreamEventFlagRootChanged)
            root_changed = true;
        if (event_ids[i])
            root_change_at = std::min(root_change_at, event_ids[i]);
        paths.push_back(FilePath(
            reinterpret_cast<char**>(event_paths)[i])
                            .StripTrailingSeparators());
    }

    // Reinitialize the event stream if we find changes to the root. This is
    // necessary since FSEvents doesn't report any events for the subtree after
    // the directory to be watched gets created.
    if (root_changed) {
        // Resetting the event stream from within the callback fails (FSEvents spews
        // bad file descriptor errors), so post a task to do the reset.
        g_task_runner.Get().PostTask(
            FROM_HERE,
            Bind(&FilePathWatcherFSEvents::UpdateEventStream, watcher,
                root_change_at));
    }

    watcher->OnFilePathsChanged(paths);
}

FilePathWatcherFSEvents::~FilePathWatcherFSEvents()
{
    // This method may be called on either the libdispatch or task_runner()
    // thread. Checking callback_ on the libdispatch thread here is safe because
    // it is executing in a task posted by Cancel() which first reset callback_.
    // PostTask forms a sufficient memory barrier to ensure that the value is
    // consistent on the target thread.
    DCHECK(callback_.is_null())
        << "Cancel() must be called before FilePathWatcher is destroyed.";
}

void FilePathWatcherFSEvents::OnFilePathsChanged(
    const std::vector<FilePath>& paths)
{
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
    DCHECK(!resolved_target_.empty());
    task_runner()->PostTask(
        FROM_HERE, Bind(&FilePathWatcherFSEvents::DispatchEvents, this, paths, target_, resolved_target_));
}

void FilePathWatcherFSEvents::DispatchEvents(const std::vector<FilePath>& paths,
    const FilePath& target,
    const FilePath& resolved_target)
{
    DCHECK(task_runner()->RunsTasksOnCurrentThread());

    // Don't issue callbacks after Cancel() has been called.
    if (is_cancelled() || callback_.is_null()) {
        return;
    }

    for (const FilePath& path : paths) {
        if (resolved_target.IsParent(path) || resolved_target == path) {
            callback_.Run(target, false);
            return;
        }
    }
}

void FilePathWatcherFSEvents::CancelOnMessageLoopThread()
{
    // For all other implementations, the "message loop thread" is the IO thread,
    // as returned by task_runner(). This implementation, however, needs to
    // cancel pending work on the Dispatch Queue thread.
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());

    if (fsevent_stream_) {
        DestroyEventStream();
        target_.clear();
        resolved_target_.clear();
    }
}

void FilePathWatcherFSEvents::UpdateEventStream(
    FSEventStreamEventId start_event)
{
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());

    // It can happen that the watcher gets canceled while tasks that call this
    // function are still in flight, so abort if this situation is detected.
    if (resolved_target_.empty())
        return;

    if (fsevent_stream_)
        DestroyEventStream();

    ScopedCFTypeRef<CFStringRef> cf_path(CFStringCreateWithCString(
        NULL, resolved_target_.value().c_str(), kCFStringEncodingMacHFS));
    ScopedCFTypeRef<CFStringRef> cf_dir_path(CFStringCreateWithCString(
        NULL, resolved_target_.DirName().value().c_str(),
        kCFStringEncodingMacHFS));
    CFStringRef paths_array[] = { cf_path.get(), cf_dir_path.get() };
    ScopedCFTypeRef<CFArrayRef> watched_paths(CFArrayCreate(
        NULL, reinterpret_cast<const void**>(paths_array), arraysize(paths_array),
        &kCFTypeArrayCallBacks));

    FSEventStreamContext context;
    context.version = 0;
    context.info = this;
    context.retain = NULL;
    context.release = NULL;
    context.copyDescription = NULL;

    fsevent_stream_ = FSEventStreamCreate(NULL, &FSEventsCallback, &context,
        watched_paths,
        start_event,
        kEventLatencySeconds,
        kFSEventStreamCreateFlagWatchRoot);
    FSEventStreamSetDispatchQueue(fsevent_stream_,
        g_task_runner.Get().GetDispatchQueue());

    if (!FSEventStreamStart(fsevent_stream_)) {
        task_runner()->PostTask(
            FROM_HERE, Bind(&FilePathWatcherFSEvents::ReportError, this, target_));
    }
}

bool FilePathWatcherFSEvents::ResolveTargetPath()
{
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
    FilePath resolved = ResolvePath(target_).StripTrailingSeparators();
    bool changed = resolved != resolved_target_;
    resolved_target_ = resolved;
    if (resolved_target_.empty()) {
        task_runner()->PostTask(
            FROM_HERE, Bind(&FilePathWatcherFSEvents::ReportError, this, target_));
    }
    return changed;
}

void FilePathWatcherFSEvents::ReportError(const FilePath& target)
{
    DCHECK(task_runner()->RunsTasksOnCurrentThread());
    if (!callback_.is_null()) {
        callback_.Run(target, true);
    }
}

void FilePathWatcherFSEvents::DestroyEventStream()
{
    FSEventStreamStop(fsevent_stream_);
    FSEventStreamInvalidate(fsevent_stream_);
    FSEventStreamRelease(fsevent_stream_);
    fsevent_stream_ = NULL;
}

void FilePathWatcherFSEvents::StartEventStream(FSEventStreamEventId start_event,
    const FilePath& path)
{
    DCHECK(g_task_runner.Get().RunsTasksOnCurrentThread());
    DCHECK(resolved_target_.empty());

    target_ = path;
    ResolveTargetPath();
    UpdateEventStream(start_event);
}

} // namespace base
